Push the right or left arrows to progress from topic to topic.
Push the up or down arrows to navigate within a topic.
This presentation has many interactive figures, some browsers may have trouble displaying all of them. If you find an image that appears not to have loaded correctly press F5 on your keyboard or reload the page. This way the presentation will stay on the same slide and reload the figure. Some figures may display improperly on mobile devices.
Portfolio management strategies can bring with them some heated discussions. Which metric is the most important? What is the best way to optimize a portfolio? How to choose our assets?
This project implements a Portfolio Management Strategy Simulator that tests a variety of different possible management strategies.
The asset selection is based on a ranking function which serves as a metric to evaluate the possible stocks candidates and selects only a subset of them to then optimize the portfolio with the selected stocks.
Smart beta strategies are usually referred to as a mid-term strategy between active investing and passive investing. An active investing strategy requires from the investor an active role in picking the assets to invest, while a passive strategy requires almost no active roll [1].
A smart beta strategy is therefore based on a set of fixed rules that are usually automatically applied to portfolio operations and ensure that the portfolio will follow a predefined behavior based on a range of factors. This is why a smart-beta strategy is also sometimes referred to as a factor investing.
The factors to be chosen for the rules of the portfolio operation are varied, some are based on technical analysis metrics, such as the Nifty 200 Momentum 30 which selects a portfolio of assets based on momentum, others smart-beta strategies may take factors such as metrics from fundamental analysis, credit scores, volatility or even sustainability and moral factors into consideration for asset selection [2][3].
With this in mind, this project will implement a Portfolio Management Strategy Simulator that weekly selects and invest all avaliable capital in a subset of selected assets from an bigger universe of candidate assets by ranking the canditate assets with a generic ranking function $f$ defined in this project as:
$$f(\text{stock price history}, t)\rightarrow m$$Where $t$ the number of trading calendar days into the past to take into account to get the score $m$ for the $\text{stock price history}$ up to the present moment in the simulation.
With this ranking function, one can propose a selection of the top N stocks to where to allocate funds. To know how much of the available capital to allocate to each selected asset one could use the ranking itself but there are already well know optimization techniques for portfolios. In the current implementation of the simulator, the available options are to optimize to get the Maximum Sortino Ratio (MSR) portfolio or the Global Minimum Volatility (GMV) portfolio.
Optimizing for the chosen subset of assets using the ranking function lets the simulator reduce the size of the state space for the possible portfolio capital allocations and therefore require less computational power and will be quicker to process.
Rankings are thoroughly used in society and are a way to reduce the complexity of a system into a simpler metric that can be used to compare elements within the system [4].
Rankings are used everywhere, from fields like sports (e.g. the NFL's top 100 player rankings) to sociology, where one could measure the influence of people by ranking them by the number of Instagram followers going all the way to money and power, where one can get a list like the Forbes annual richest list.
Since many ranks usually are really big or don't have clear bounds (for example the actual complete ranking for the richest persons) in practice many rankings are further simplified to a top-N ranking.
So far the factors implemented to rank the assets are:
None-the-less, the simulator is implemented in such a way where other functions can be easily applied in future work.
The algorithm of the Portfolio Management Strategy Simulator is the following:
- Define algorithm hyperparameters
- Every chosen weekday (if possible):
- Get the top N stocks for the day according to rank by criteria and t
- Optimize portfolio weights according to the chosen technique and window
- Allocate all available capital (Buy stocks)
The universe of possible stock candidates is the S&P500
All historic prices analyzed are closing prices of the day
For simplicity, we will not consider transaction fees
The expected return will be estimated as the annualized return in the window of analysis
The expected return and variance-covariance matrix are calculated with data from the windows of analysis
We will assume we can buy hole stocks and fractions of stocks
We will allocate all available capital each week
We can't take short positions
We will trade weekly on the specified weekday by the user. If that day of the week the markets are close the portfolio will hold the previous week's assets until the next tradeable day and resume the algorithm
The current version of this simulator lets the user specify the following hyperparameters:
The simulation also lets the user track the allocations and close market days to further study portfolio behavior and increase behavior traceability.
The recorded portfolio behavior is compared to the S&P500 and the risk-free rate.
The S&P500 is a major index that comprises the 500 most capitalized companies in the United States.
This index is sometimes perceived by institutional investors as a more representative index of the whole US economy than other indexes as the S&P500 collects more stocks from many companies in all sectors [5].
The overall S&P index looks like this [6]:
SP500_index_plot
These are the S&P500 companies, their stock symbol and their economy sector [7]:
SP500_metadata
| Symbol | Name | Sector | |
|---|---|---|---|
| 0 | MMM | 3M | Industrials |
| 1 | AOS | A. O. Smith | Industrials |
| 2 | ABT | Abbott Laboratories | Health Care |
| 3 | ABBV | AbbVie | Health Care |
| 4 | ABMD | Abiomed | Health Care |
| ... | ... | ... | ... |
| 500 | YUM | Yum! Brands | Consumer Discretionary |
| 501 | ZBRA | Zebra Technologies | Information Technology |
| 502 | ZBH | Zimmer Biomet | Health Care |
| 503 | ZION | Zions Bancorp | Financials |
| 504 | ZTS | Zoetis | Health Care |
505 rows Ć 3 columns
Here we have a tree map showing the composition of the S&P500 by sector and company:
s_and_p_500_composition
The risk-free rate, most commonly defined as the return rate one would get from government bonds, is taken in practice as the return one would get with risk 0.
The risk 0 assumption could be debatable, yet most would agree that it is easier for any other entity to default than governments.
The USA risk-free rate from early 1962 to april 2022 looks like this:
risk_free_rate_plot
Let's introduce our mini-universe ("miniverse") of stocks to illustrate some metrics and portfolio construction techniques in the following subsections. To illustrate the theoric framework we will only work with data from 2015 to May 2022.
The selected stocks for this miniverse example is comprised of the top 10 component companies of the S&P 500:
| Top Component # | Company | Description |
|---|---|---|
| 1 | AAPL | Apple |
| 2 | MSFT | Microsoft |
| 3 | AMZN | Amazon |
| 4 | TSLA | Tesla |
| 5 | GOOGL | Google Class A (with vote rights) |
| 6 | GOOG | Google Class C (without vote rigths) |
| 7 | NVDA | Nvidia Corp. |
| 8 | BRK-B | Berkshire Hathaway Inc. Class B |
| 9 | FB | Meta Platforms Inc. Class A |
| 10 | UNH | UnitedHealth Group Incorporated |
The prices the miniverse looked like this:
miniverse_prices_plot
A very useful representation to study historic prices for investemt purposes is the return. The return is defined as the ratio between the prices of an asset two points in time.
From a sequence of prices ($\mathbf{P}$) we can calculate the return as between the price at time $t$ and $t+1$ as:
$$R_{t,t+1} = \frac{P_{t+1} - P_t}{P_t} = \frac{P_{t+1}}{P_t} -1 $$The Python we can get the returns of a price series using Pandas and the pct_change method as follows:
def get_returns(prices):
returns = prices.pct_change()
returns.index = pd.to_datetime(returns.index)
if isinstance(returns, pd.DataFrame):
return returns.iloc[1:,:]
if isinstance(returns, pd.Series):
return returns.iloc[1:]
The daily returns for the miniverse looks like this:
returns_plot
Another useful metric is the compounded return that captures the cumulative return over a series of returns, this is defined as the product of the returns represented with respect to 1 (1 represents a null profit and below 1 represents a loss).
The Python function that computes this metric is:
def get_compounded_return(returns):
"""
Get compounded return
Returns compounded return centered around 0
"""
return ((returns + 1).prod() - 1)
The compounded returns for each stock in the miniverse from 2015 to May 2022 are:
get_compounded_return(returns)
AAPL 4.575355 MSFT 5.157999 AMZN 5.974654 TSLA 14.136109 GOOGL 3.113228 GOOG 3.177249 NVDA 33.501385 BRK-B 1.038278 FB 1.467049 UNH 4.374148 dtype: float64
In finances the convention is to represent most metrics annualized, therefore we need to annualize discrete returns.
To annualize the returns we define the following function:
def annualize_returns(returns, periods_per_year = 252):
"""
This function recieves a returns dataframe
"""
compounded_growth = (1 + returns).prod()
n_periods = returns.shape[0]
return compounded_growth**(periods_per_year/n_periods)-1
The miniverse mean annual return is:
annualize_returns(returns)
AAPL 0.262299 MSFT 0.279422 AMZN 0.301203 TSLA 0.445303 GOOGL 0.211314 GOOG 0.213852 NVDA 0.616085 BRK-B 0.101343 FB 0.130218 UNH 0.256025 dtype: float64
When we have a portfolio of assets with a certain relative weight each one with respect to the whole portfolio we can compute the overall portfolio return as a weighted average (or dot product).
The following function computes the portfolio return for a couple of weights and returns vectors where every entry represents a stock in the portfolio:
def portfolio_return(weights, returns):
"""
Computes the return on a portfolio from constituent
returns and weights weights are a numpy array or
Nx1 matrix and returns are a numpy array or Nx1 matrix
"""
return weights.T @ returns
If we had an equally weighted portfolio of the miniverse we would have had the following portfolio return on the last trading day in the dataset:
weights = np.repeat(1/returns.shape[1], returns.shape[1])
portfolio_return(weights, returns.iloc[-1,:])
-0.008715474631853725
There are many risk metrics in portfolio analysis, next I present some
In Modern Portfolio Theory (MPT) volatility is defined as the standard deviation of the returns:
def get_volatility(returns):
return returns.std()
The mean daily volatility of the miniverse is:
get_volatility(returns)
AAPL 0.018502 MSFT 0.017319 AMZN 0.020039 TSLA 0.035343 GOOGL 0.017079 GOOG 0.017086 NVDA 0.029623 BRK-B 0.012665 FB 0.021748 UNH 0.016872 dtype: float64
In Post-Modern Portfolio Theory (PMPT) volatility only takes into account negative returns, this is also known as a semideviation.
This is one of the main swifts in paradigms between MPT and PMPT.
A Python function that gets the semideviation is:
def semideviation(returns):
negative_returns = returns[returns < 0 ]
return negative_returns.std()
The mean daily volatility for negative returns of the miniverse is:
semideviation(returns)
AAPL 0.013678 MSFT 0.012995 AMZN 0.014214 TSLA 0.024626 GOOGL 0.012416 GOOG 0.012390 NVDA 0.020783 BRK-B 0.009564 FB 0.017724 UNH 0.012497 dtype: float64
We can see that the assets have lower volatility in the PMPT ideology.
Once again, standard practices require us to report results annualized.
For modern portfolio theory we can get the annualized volatility as follows:
def annualize_vol(returns, periods_in_year:int = 252):
volatility = returns.std()
return volatility*np.sqrt(periods_in_year)
The mean annualized volatility of the miniverse is:
annualize_vol(returns)
AAPL 0.293710 MSFT 0.274926 AMZN 0.318114 TSLA 0.561056 GOOGL 0.271116 GOOG 0.271233 NVDA 0.470246 BRK-B 0.201044 FB 0.345235 UNH 0.267839 dtype: float64
For modern portfolio theory we can get the annualized volatility as follows:
def annualize_semideviation(returns, periods_in_year:int = 252):
volatility = semideviation(returns)
return volatility*np.sqrt(periods_in_year)
The mean annualized semideviation of the miniverse is:
annualize_semideviation(returns)
AAPL 0.217128 MSFT 0.206288 AMZN 0.225633 TSLA 0.390925 GOOGL 0.197092 GOOG 0.196691 NVDA 0.329914 BRK-B 0.151825 FB 0.281356 UNH 0.198391 dtype: float64
The value at risk is another useful and common risk measure.
Is the worst-case scenario of the return distribution after eliminating a given percentile. It is usually represented as a positive number.
There are a few ways to define the value at risk:
Uses historical data to predict the future. Uses the actual series of values.
The problem with this kind of model is that they rely on assuming past data can be used to model present and future times.
def var_historic_from_prices(p, t=0, level=5):
"""
Returns the historic Value at Risk at a specified level
i.e. returns the number such that "level" percent of the returns
fall below that number, and the (100-level) percent are above
t is the number of weeks in the past to take into account
"""
if isinstance(p, pd.DataFrame):
r = get_returns(p)
return r.aggregate(var_historic, t=t, level=level)
elif isinstance(p, pd.Series):
return -np.percentile(p.iloc[-t:], level)
else:
raise TypeError("Expected prices to be a Series or DataFrame")
For the miniverse the historic var is:
var_historic_from_prices(example_prices, t=100)
AAPL 0.033360 MSFT 0.038976 AMZN 0.056385 TSLA 0.070608 GOOGL 0.037822 GOOG 0.037541 NVDA 0.068208 BRK-B 0.018621 FB 0.051885 UNH 0.026476 dtype: float64
We select a return distribution model (gaussian for simplicity), in this way, we only need to determine the parameters for the model ($\mu, \sigma$).
$$VaR(1-\alpha)= -(\mu + z_\alpha \sigma)$$Where $\alpha$ is the confidence leve, $z_\alpha$ is the $\alpha$-quantile of the standard normal distribution.
The problem with this methodology is that it incurs in a greater model error since real-world data is rarely normally distributed.
It is implemented as follows with the Cornish-Fisher modification explained in posterior slides:
def var_gaussian(r, level=5, modified=False):
"""
Returns the Parametric Gaussian VaR of a Series or DataFrame
If "modified" is True, then the modified VaR is returned,
using the Cornish-Fisher modification
"""
# compute the Z score assuming it was Gaussian
z = norm.ppf(level/100)
if modified:
# modify the Z score based on observed skewness and kurtosis
s = scipy.stats.skew(r)
k = scipy.stats.kurtosis(r) + 3
z = (z +
(z**2 - 1)*s/6 +
(z**3 -3*z)*(k-3)/24 -
(2*z**3 - 5*z)*(s**2)/36
)
return -(r.mean() + z*r.std(ddof=0))
For the miniverse the gaussian VaR is:
var_gaussian(returns, level=5, modified=False)
AAPL 0.029329 MSFT 0.027351 AMZN 0.031708 TSLA 0.056034 GOOGL 0.027178 GOOG 0.027182 NVDA 0.046370 BRK-B 0.020362 FB 0.035036 UNH 0.026697 dtype: float64
We could model the returns as other distribuion other than normal, e.g. Pareto. We could also used other more complex models. These are not yet implemented at this version of the simulator.
Cornish-Fisher VaR
Uses a semi-parametric model.
Uses a polynomial expansion for the $\alpha$-quantile that takes into account the skewness and kurtosis.
For the miniverse the gaussian var with the Cornish-Fisher modification is:
var_gaussian(returns, level=5, modified=True)
AAPL 0.027594 MSFT 0.023749 AMZN 0.025835 TSLA 0.049888 GOOGL 0.022234 GOOG 0.021923 NVDA 0.036158 BRK-B 0.017344 FB 0.031376 UNH 0.022431 dtype: float64
Another metric that can be used to measure risk is the conditional VaR, which is defined as the mean of the returns set aside on the normal VaR. This provides a risk measure directedly linked to the negative heavy-tailed risks.
def cvar_historic(r, t=0, level=5):
"""
Computes the Conditional VaR of Series or DataFrame
"""
if isinstance(r, pd.Series):
is_beyond = r <= -var_historic(r, t=t, level=level)
return -r[is_beyond].mean()
elif isinstance(r, pd.DataFrame):
return r.aggregate(cvar_historic, t=t, level=level)
else:
raise TypeError("Expected r to be a Series or DataFrame")
The conditional VaR for the miniverse is:
cvar_historic(returns)
AAPL 0.042433 MSFT 0.039794 AMZN 0.045137 TSLA 0.078006 GOOGL 0.039227 GOOG 0.039217 NVDA 0.064911 BRK-B 0.029120 FB 0.050532 UNH 0.036754 dtype: float64
In practice we represent the overall portfolio volatility as the matrix multiplication:
$$\text{portfolio volatility} = (W^T \Sigma W)^{\frac{1}{2}}$$Where W is the weight vector of the portfolio and $\Sigma$ is the variace-covariance of the returns.
def portfolio_vol(weights, covmat):
"""
Computes the vol of a portfolio from a covariance matrix
and constituent weights. The weights are a numpy array
or N x 1 matrix and covmat is an N x N matrix
"""
return (weights.T @ covmat @ weights)**0.5
The portfolio volatility for the whole miniverse assuming an equally weighted portfolio is:
weights = np.repeat(1/returns.shape[1], returns.shape[1])
portfolio_vol(weights, returns.cov())
0.015230711119188311
In investing seeking $\alpha$ is one of the most important activities.
$\alpha$ is defined as the difference between a portfolio or asset and the risk free rate. It is a measure of how much additional return one could get over the one that is practically free.
def get_alpha(rp, rf):
return rp - rf
The $\alpha$ for each stock in the miniverse for 2020 was:
a_returns = annualize_returns(returns["2020":"2021"])
get_alpha(a_returns, risk_free.loc["2020-01-06"]["Risk Free Rate"])
AAPL 0.549964 MSFT 0.457684 AMZN 0.327114 TSLA 2.529690 GOOGL 0.454176 GOOG 0.454601 NVDA 1.219862 BRK-B 0.133235 FB 0.264104 UNH 0.310732 dtype: float64
When analyzing an asset it is problematic to just take into account the returns. The returns say one thing but it is also important to take into account the risk.
The most useful ratio for this is to consider the ratio between the $\alpha$ and the volatility.
In modern portfolio theory this ratio is named the Sharpe ratio and is implemented in Python as follows:
def sharpe_ratio(r, riskfree_rate, periods_per_year=252):
"""
Computes the annualized sharpe ratio of a set of returns
"""
# convert the annual riskfree rate to per period
rf_per_period = (1+riskfree_rate)**(1/periods_per_year)-1
excess_ret = r - rf_per_period
ann_ex_ret = annualize_returns(excess_ret, periods_per_year)
ann_vol = annualize_vol(r, periods_per_year)
return ann_ex_ret/ann_vol
The annualized Sharp ratio for each stock the miniverse in 2020 was:
sharpe_ratio(returns.loc["2020":"2021"], riskfree_rate = risk_free.loc["2020-01-06"]["Risk Free Rate"])
AAPL 1.445925 MSFT 1.310273 AMZN 1.001835 TSLA 3.353569 GOOGL 1.390704 GOOG 1.405644 NVDA 2.321037 BRK-B 0.486359 FB 0.674117 UNH 0.837195 dtype: float64
The equivalent metric in post-modern portfolio theory is named sortino ratio and is implemented as follows:
def sortino_ratio(r, riskfree_rate, periods_per_year=252):
"""
Computes the annualized sharpe ratio of a set of returns
"""
# convert the annual riskfree rate to per period
rf_per_period = (1+riskfree_rate)**(1/periods_per_year)-1
excess_ret = r - rf_per_period
ann_ex_ret = annualize_returns(excess_ret, periods_per_year)
ann_vol = annualize_semideviation(r, periods_per_year)
return ann_ex_ret/ann_vol
The annualized Sortino ratio for each stock in the miniverse in 2020 was:
sortino_ratio(returns.loc["2020":"2021"], riskfree_rate = risk_free.loc["2020-01-06"]["Risk Free Rate"])
AAPL 1.975030 MSFT 1.706131 AMZN 1.495981 TSLA 4.858410 GOOGL 1.809783 GOOG 1.814730 NVDA 3.372404 BRK-B 0.608032 FB 0.949627 UNH 1.058362 dtype: float64
Where $P$ is the current price and $P_{t}$ is the price $t$ days ago:
def get_momentum(prices, t):
return prices - prices.shift(t)
The momentum for the miniverse components using $t=10$ is:
momentum_plot
The momentum for the hole S&P500 index using $t=100$ is:
sp500_momentum_plot
When we have two or more assets we can combine them to reduce the risk we would have with a single asset as long as the assets are not perfectly correlated. This is known as the only free lunch of finances.
These are possible portfolios one can make with 2 assets, GOOGL and MSFT, if one can buy fractions of stocks one could create any portfolio along the line:
two_assets_combination
With N-assets we dispose of a bigger space state to create portfolios.
To reduce the possible portfolios let's focus on those who provide the minimal volatility for a given target return.
For this, we use the next function that uses a scipy minimizer to find the weights for such a portfolio.
def minimize_vol(target_return, er, cov):
"""
Returns the optimal weights that achieve the target return
given a set of expected returns and a covariance matrix
"""
n = er.shape[0]
init_guess = np.repeat(1/n, n)
bounds = ((0.00, 1),) * n # an N-tuple of 2-tuples!
# construct the constraints
weights_sum_to_1 = {'type': 'eq',
'fun': lambda weights: np.sum(weights) - 1
}
return_is_target = {'type': 'eq',
'args': (er,),
'fun': lambda weights, er: target_return - portfolio_return(weights,er)
}
weights = minimize(portfolio_vol, init_guess,
args=(cov,), method='SLSQP',
options={'disp': False},
constraints=(weights_sum_to_1,return_is_target),
bounds=bounds)
return weights.x
We can plot the possible portfolio returns and volatilities for the miniverse assuming a risk-free rate equal to the one on 2015-01-06:
plot_ef(er, cov)
Note that we could construct any portfolio inside this curve, nonetheless, we are interested in the upper side of the curve as these portfolios have the maximum return for the minimum volatility.
This frontier of portfolios with maximum return and minimum volatility is known as the efficient frontier.
We can construct the maximum return/risk portfolio. This portfolio maximizes the reward per unit of risk but requires expected return estimations [8].
For a portfolio:
$$SR_p = \frac{\mu_p - r_f}{\sigma_p} = \frac{\sum_{i=1}^N w_i \mu_i - r_f}{\sqrt{\sum_{i,j=1}^N w_i w_j \sigma_i \sigma_j \rho_{ij}}}$$Where $SR_p$ is the Sortino ratio of the portfolio $\mu_p = \sum_{i=1}^N w_i \mu_i$ its the mean expected return $r_f$ its the risk-free rate $\sigma_i,\sigma_j$ are the volatilities of assets $i$ and $j$, $\rho_{ij}$ its the correlation of $i$ and $j$.
The Python implementation to get the maximum Sharp ratio portfolio weights is:
def msr(riskfree_rate, er, cov, min_w = 0.01, max_w = 0.95):
"""Returns the weights of the portfolio that gives you the maximum sharpe ratio
given the riskfree rate and expected returns and a covariance matrix"""
n = er.shape[0]
init_guess = np.repeat(1/n, n)
bounds = ((min_w, max_w),) * n # an N-tuple of 2-tuples!
# construct the constraints
weights_sum_to_1 = {'type': 'eq',
'fun': lambda weights: np.sum(weights) - 1
}
def neg_sharpe(weights, riskfree_rate, er, cov):
"""Returns the negative of the sharpe ratio
of the given portfolio"""
r = portfolio_return(weights, er)
vol = portfolio_vol(weights, cov)
return -(r - riskfree_rate)/vol
weights = minimize(neg_sharpe, init_guess,
args=(riskfree_rate, er, cov), method='SLSQP',
options={'disp': False},
constraints=(weights_sum_to_1,),
bounds=bounds)
return weights.x/weights.x.sum()
The maximum Sharpe ratio portfolio is shown as the red dot in the next figure. Note that the MSR is not exactly on the frontier as we imposed a minimum of 1% in all selected stocks and a maximum of 95%. Many portfolios in the efficient frontier have zero weighted components.
plot_ef(er, cov, show_msr=True, riskfree_rate=riskfree_rate)
Estimations of expected returns are usually worst than estimations for the risk.
To get a minimum risk portfolio we can simply get the maximum Sharpe ratio portfolio when all the expected returns are the same. This trick works because if the expected returns are the same the maximum Sharpe ratio must be obtained purely by minimizing the volatility [8].
def gmv(cov, min_w = 0.01, max_w = 0.95):
"""
Returns the weights of the Global Minimum Volatility portfolio
given a covariance matrix
"""
n = cov.shape[0]
return msr(0, np.repeat(1, n), cov, min_w = 0.01, max_w = 0.95)
A nice characteristic of this portfolio optimization is that it doesn't require expected return estimates, which are usually harder to predict than volatility.
The Maximum Sharpe Ratio portfolio is shown in red while the Global Minimum Volatility portfolio is shown in green:
plot_ef(er, cov, show_msr=True, riskfree_rate=riskfree_rate, show_gmv=True)
To construct an equally weighted portfolio we only need to assign a weight of $\frac{1}{N}$ to each of the $N$ portfolio components.
def get_eq_weighted_portfolio(expected_returns):
number_of_assets = len(expected_returns)
return np.repeat(1/number_of_assets, number_of_assets)
The maximum Sortino ratio portfolio is shown in red, the Global Minimum Volatility portfolio is shown in green and the equally weighted portfolio is shown in yellow:
plot_ef(er, cov, show_msr=True, riskfree_rate=riskfree_rate, show_ew=True, show_gmv=True)
Let's define some functions that will be useful to evaluate our portfolio.
We need a function to get the capitalization of our portfolios and to compare them to the benchmarks.
First of all, let's define a function that given the universe of stocks, the day, the stocks owned and the number of stocks owned returns the portfolio value for that particular portfolio on that particular day.
def get_portfolio_cap(universe, day, stocks, number_of_stocks_owned):
prices = universe.loc[day,stocks]
valuation = pd.Series(number_of_stocks_owned@prices, index=[day])
valuation.index = pd.to_datetime(valuation.index)
return valuation
What a portfolio capitalization would be on March 16th, 2021 if it had 100 stocks from Tesla and 50 stocks from Microsoft
get_portfolio_cap(universe, "2021-03-16", ["TSLA","MSFT"], [100,50])
2021-03-16 79450.075531 dtype: float64
We need a way to compare the portfolio to the benchmark, for this let's compare them with their annualized returns and volatility.
We can define a function that returns a comparison table between a portfolio returns series and the benchmarks
A portfolio that consisted purely of Meta stocks (FB) would compare to the S&P500 and the risk-free rate in the following way:
meta_returns = returns["FB"]
eval_portfolio(meta_returns, p_periods_per_year = 252)
| Annual Avg | Portfolio | S&P500 Benchmark | RFR Benchmark | Portfolio - S&P500 | Portfolio - RFR |
|---|---|---|---|---|---|
| Start date: 2015-01-05. End date: 2022-05-20 | |||||
| Return | 0.130218 | 0.092195 | 0.010037 | 0.038023 | 0.120181 |
| Volatility | 0.345235 | 0.182862 | 0.000000 | 0.162374 | 0.345235 |
| Semideviation | 0.281356 | 0.157105 | 0.000000 | 0.124251 | 0.281356 |
Let's remember the algorithm of the portfolio strategy simulator:
- Define algorithm hyperparameters
- Every chosen weekday (if possible):
- Get the top N stocks for the day according to rank by criteria and t
- Optimize portfolio weights according to the chosen technique and window
- Allocate all available capital (Buy stocks)
Let's implement the class in the next Gist that has all the methods shown we need so we can simply define the hyperparameters and then run the simulation.
The next slides contain the results of investing 10 million USD dollars using the smart-beta ranking strategy.
All portfolios select the top 20 companies in the ranking selection process.
All portfolio strategies start and end at the same date:
Start date: First trading day of 2014
Ent date: Last price record date in dataset - May 2022
Let's create a random ranking function to use as a reference:
def random_ranking_func(prices, t=0):
return prices.apply(lambda serie: np.random.random())
random_ranking_func(universe)
MMM 0.839168
AOS 0.004708
ABT 0.213204
ABBV 0.712022
ABMD 0.437319
...
ZION 0.945058
ZTS 0.557707
BRK-B 0.329514
BF-B 0.663518
WTW 0.310729
Length: 501, dtype: float64
p_eval_1
| Annual Avg | Portfolio | S&P500 Benchmark | RFR Benchmark | Portfolio - S&P500 | Portfolio - RFR |
|---|---|---|---|---|---|
| Start date: 2014-01-13. End date: 2022-05-16 | |||||
| Return | 0.191221 | 0.095439 | 0.008998 | 0.095782 | 0.182223 |
| Volatility | 0.202313 | 0.176203 | 0.000000 | 0.026109 | 0.202313 |
| Semideviation | 0.153240 | 0.151236 | 0.000000 | 0.002003 | 0.153240 |
The portfolio valuation over time looked like this:
portfolio_plot
The portfolio allocation looked like this in terms of weights:
allocations_plot_w
p_eval_2
| Annual Avg | Portfolio | S&P500 Benchmark | RFR Benchmark | Portfolio - S&P500 | Portfolio - RFR |
|---|---|---|---|---|---|
| Start date: 2014-01-13. End date: 2022-05-16 | |||||
| Return | 0.159721 | 0.095439 | 0.008998 | 0.064282 | 0.150723 |
| Volatility | 0.158991 | 0.176203 | 0.000000 | -0.017212 | 0.158991 |
| Semideviation | 0.144826 | 0.151236 | 0.000000 | -0.006411 | 0.144826 |
The portfolio valuation over time looked like this:
portfolio_plot
The portfolio allocation looked like this in terms of weights:
allocations_plot_w
p_eval_3
| Annual Avg | Portfolio | S&P500 Benchmark | RFR Benchmark | Portfolio - S&P500 | Portfolio - RFR |
|---|---|---|---|---|---|
| Start date: 2014-01-13. End date: 2022-05-16 | |||||
| Return | 0.112185 | 0.095439 | 0.008998 | 0.016746 | 0.103188 |
| Volatility | 0.172257 | 0.176203 | 0.000000 | -0.003946 | 0.172257 |
| Semideviation | 0.128078 | 0.151236 | 0.000000 | -0.023158 | 0.128078 |
The portfolio valuation over time looked like this:
portfolio_plot
The portfolio returns over time looked like this:
portfolio_returns_plot
The portfolio allocation looked like this in terms of weights:
allocations_plot_w
p_eval_4
| Annual Avg | Portfolio | S&P500 Benchmark | RFR Benchmark | Portfolio - S&P500 | Portfolio - RFR |
|---|---|---|---|---|---|
| Start date: 2014-01-13. End date: 2022-05-16 | |||||
| Return | 0.192916 | 0.095439 | 0.008998 | 0.097477 | 0.183918 |
| Volatility | 0.206926 | 0.176203 | 0.000000 | 0.030723 | 0.206926 |
| Semideviation | 0.171075 | 0.151236 | 0.000000 | 0.019839 | 0.171075 |
The portfolio valuation over time looked like this:
portfolio_plot
The portfolio returns over time looked like this:
portfolio_returns_plot
The portfolio allocation looked like this in terms of weights:
allocations_plot_w
p_eval_5
| Annual Avg | Portfolio | S&P500 Benchmark | RFR Benchmark | Portfolio - S&P500 | Portfolio - RFR |
|---|---|---|---|---|---|
| Start date: 2014-01-13. End date: 2022-05-16 | |||||
| Return | 0.156447 | 0.095439 | 0.008998 | 0.061008 | 0.147449 |
| Volatility | 0.253532 | 0.176203 | 0.000000 | 0.077329 | 0.253532 |
| Semideviation | 0.200317 | 0.151236 | 0.000000 | 0.049080 | 0.200317 |
The portfolio valuation over time looked like this:
portfolio_plot
The portfolio returns over time looked like this:
portfolio_returns_plot
The portfolio allocation looked like this in terms of weights:
allocations_plot_w
p_eval_6
| Annual Avg | Portfolio | S&P500 Benchmark | RFR Benchmark | Portfolio - S&P500 | Portfolio - RFR |
|---|---|---|---|---|---|
| Start date: 2014-01-13. End date: 2022-05-16 | |||||
| Return | 0.204720 | 0.095439 | 0.008998 | 0.109281 | 0.195723 |
| Volatility | 0.300279 | 0.176203 | 0.000000 | 0.124075 | 0.300279 |
| Semideviation | 0.206592 | 0.151236 | 0.000000 | 0.055356 | 0.206592 |
The portfolio valuation over time looked like this:
portfolio_plot
The portfolio returns over time looked like this:
portfolio_returns_plot
The portfolio allocation looked like this:
allocations_plot
The portfolio allocation looked like this in terms of weights:
allocations_plot_w
p_eval_5
| Annual Avg | Portfolio | S&P500 Benchmark | RFR Benchmark | Portfolio - S&P500 | Portfolio - RFR |
|---|---|---|---|---|---|
| Start date: 2014-01-13. End date: 2022-05-16 | |||||
| Return | 0.156447 | 0.095439 | 0.008998 | 0.061008 | 0.147449 |
| Volatility | 0.253532 | 0.176203 | 0.000000 | 0.077329 | 0.253532 |
| Semideviation | 0.200317 | 0.151236 | 0.000000 | 0.049080 | 0.200317 |
The portfolio valuation over time looked like this:
portfolio_plot
The portfolio returns over time looked like this:
portfolio_returns_plot
The portfolio allocations looked like this in terms of weights:
allocations_plot_w
p_eval_8
| Annual Avg | Portfolio | S&P500 Benchmark | RFR Benchmark | Portfolio - S&P500 | Portfolio - RFR |
|---|---|---|---|---|---|
| Start date: 2014-01-13. End date: 2022-05-16 | |||||
| Return | 0.517314 | 0.095439 | 0.008998 | 0.421875 | 0.508316 |
| Volatility | 0.481713 | 0.176203 | 0.000000 | 0.305510 | 0.481713 |
| Semideviation | 0.315405 | 0.151236 | 0.000000 | 0.164168 | 0.315405 |
The portfolio valuation over time looked like this:
portfolio_plot
The portfolio returns over time looked like this:
portfolio_returns_plot
The portfolio allocations looked like this:
allocations_plot
The portfolio allocations looked like this in terms of weights:
allocations_plot_w
Analyzing the portfolio behavior with respect to the dramatic valuation spike from 2020 to the present we can see that the portfolio invertedly heavily in Moderna MRN at the begining of the pandemic. This company had a drastic stock value increase and during the pandemic for being one of the top USA COVID vaccine manufacturers and the portfolio manage to take advantage of this.
moderna
We can also see that is the last past weeks, since early March 2022, the portfolio invested heavily in Occidental Petroleum Corporation [OXY], which is having a value appreciation because of the war.
oxy
p_eval_9
| Annual Avg | Portfolio | S&P500 Benchmark | RFR Benchmark | Portfolio - S&P500 | Portfolio - RFR |
|---|---|---|---|---|---|
| Start date: 2014-01-13. End date: 2022-05-16 | |||||
| Return | 0.400958 | 0.095439 | 0.008998 | 0.305519 | 0.391960 |
| Volatility | 0.375318 | 0.176203 | 0.000000 | 0.199115 | 0.375318 |
| Semideviation | 0.271836 | 0.151236 | 0.000000 | 0.120599 | 0.271836 |
The portfolio valuation over time looked like this:
portfolio_plot
We can see that only by choosing the assets in this way we are getting a much higher return than the market yet the MSR improved the portfolio valuation.
results_table
| Start date: 2014-01-13. End date: 2022-05-16 | Ranking Function | Optimization | Return | Alpha | Volatility | Semideviation | Sharpe Ratio | Sortino Ratio | Return Diff. with S&P500 | Volatility Diff. with S&P500 | Semideviation Diff. with S&P500 |
|---|---|---|---|---|---|---|---|---|---|---|---|
| Annualized Average | |||||||||||
| Portfolio 8 | MaxVaR | MSR | 0.517314 | 0.508316 | 0.481713 | 0.315405 | 1.073905 | 1.640159 | 0.421875 | 0.305510 | 0.164168 |
| Portfolio 9 | MaxVaR | Eq Weighted | 0.400958 | 0.391960 | 0.375318 | 0.271836 | 1.068314 | 1.475001 | 0.305519 | 0.199115 | 0.120599 |
| Portfolio 1 | Random | Eq Weighted | 0.191221 | 0.182223 | 0.202313 | 0.153240 | 0.945175 | 1.247855 | 0.095782 | 0.026109 | 0.002003 |
| Portfolio 4 | MinMomentum | GMV | 0.192916 | 0.183918 | 0.206926 | 0.171075 | 0.932293 | 1.127665 | 0.097477 | 0.030723 | 0.019839 |
| Portfolio 2 | Random | GMV | 0.159721 | 0.150723 | 0.158991 | 0.144826 | 1.004592 | 1.102850 | 0.064282 | -0.017212 | -0.006411 |
| Portfolio 6 | MinMomentum | MSR | 0.204720 | 0.195723 | 0.300279 | 0.206592 | 0.681768 | 0.990938 | 0.109281 | 0.124075 | 0.055356 |
| Portfolio 3 | MaxMomentum | GMV | 0.112185 | 0.103188 | 0.172257 | 0.128078 | 0.651268 | 0.875912 | 0.016746 | -0.003946 | -0.023158 |
| Portfolio 5 | MaxMomentum | MSR | 0.156447 | 0.147449 | 0.253532 | 0.200317 | 0.617068 | 0.780997 | 0.061008 | 0.077329 | 0.049080 |
| S&P500 Benchmark | None | None | 0.095439 | 0.086441 | 0.176203 | 0.151236 | 0.541642 | 0.631059 | 0.000000 | 0.000000 | 0.000000 |
| Portfolio 7 | MinVaR | MSR | 0.066175 | 0.057177 | 0.159461 | 0.140436 | 0.414990 | 0.471209 | -0.029264 | -0.016742 | -0.010801 |
| RFR Benchmark | None | None | 0.008998 | 0.000000 | 0.000000 | 0.000000 | nan | nan | -0.086441 | -0.176203 | -0.151236 |
From the table we can see that contrary to the first impression, the maximum momentum was worst than the minimum momentum and that the maximum VaR was better than the minimum VaR. Remember that for the momentum ranking function $t=10$ while for the VaR ranking function $t=252$.
I have to confess I didn't expect to have these results. I hypothesized that bigger momentum or minimum VaR were going to be the best portfolios. I didn't even intend to implement simulation for minimum momentum or maximum VaR, yet after trying, viewing the results, and double-checking my implementation I was very surprised that these portfolios were the most profitable.
After some thought my explication for these results is that maybe allocating capital to maximum momentum companies in the last 10 days didn't capture the same increment in the future, while by investing in companies that had recent prices losses we profited from regressions to the mean of the price.
A similar phenomenon may explain why investing in companies with a high historic VaR in the past year was profitable as these companies had recent high negative returns and the algorithm take advange of market corrections.
This project presents a portfolio management strategy simulator inspired by smart-beta strategies and ranking selection.
The simulator that this project presents is based on periodically ranking the top N stocks of a universe of possible assets and constructing a portfolio with them. The ranking function can be created ad-hoc or can be one of many popular metrics.
This project presents the implementation of a versatile portfolio strategy and its simulation. The simulator can be tuned with many user-defined hyperparameters.
This project has explored only 3 ranking functions so far. Many others may provide a better signal for asset selection.
This project implemented three different portfolio optimization techniques, including the equally weighted portfolio, Maximum Sortino Ratio, and Minimum Global Volatility portfolios.
By letting the user choose which optimization technique it wants to use this implementation provides an additional grade of versatility.
The simulator put to the test 8 different portfolio management strategies.
The higher return portfolio had an excess return above the S&P500 average of 42% and had 30% more volatility. Nonetheless, this portfolio only had twice as much semideviation as the S&P500.
Investing in stocks with a recent minimum momentum and a maximum historic VaR resulted in better portfolio returns.
Since the explicability of the decision making in portfolio management and understanding of portfolio behavior are two great qualities in investing this implementation follows clear rules for the selection and allocation of assets.
This implementation also provides historic data on how the allocation of capital would have been in the strategy.
This project uses a simple factor approach to rank the assets, yet in the future, more complex ranking functions that provide a multifactorial approach could be implemented, such as neural networks that could take into account a variety of technical, economical, and fundamental data.
Expected returns, which are used in MSR portfolio optimizations are currently based on historic data. Other more robust forms of returns estimations can be implemented
Implementation of other ranking functions is easy, therefore, other ranking functions, such as some risk metrics or ranking functions that take into account fundamental analysis can be relatively quickly implemented.
This project can be used to grid search for the best hyperparameters, ranking functions, and portfolio optimization techniques. In this way, one could explore how different combinations could interact in the portfolio behavior. This process can be accelerated using parallel computing.
Other constraints could be applied for constraining stock selection such as maximum volatility, an industry area max capitalization, etc.
To speed up the implementation we could change Pandas to pure NumPy. A difficulty for this is greatly the dependence on Pandas DateTimeIndex.
Reduce the assumptions made to model portfolio behavior
Dynamics of Ranking - Nature Communications - IƱuiguez et at - 2022